On Monday evening, a big chunk of Portugal went dark.
Around 17:30 the complaints started, and by 18:20 millions lost broadband, mobile data, LTE phone calls, TV apps, and anything internet related.
Then the arguing started. MEO said "mass attack". Some network people on X said "no, it's a BGP leak, look at Cloudflare Radar". Other people said "it's probably just their infrastructure falling over again".
I spent an evening actually checking. Not reading takes, querying real data and this post is what I found:
Spoiler: almost everyone was a little bit right and a little bit wrong.
First, what even is BGP?
Here's the thing that surprises people: the internet has no map.
There's no central server that knows where netflix.com lives. Instead there are about 80,000 independent networks: your ISP, Google, a university, a bank. Each one is called an Autonomous System, or AS. Each has a number: MEO's residential network is AS3243, Cloudflare is AS13335, and so on.
These networks stay in touch by constantly gossiping to each other:
"Hey, I can reach these addresses. Send me traffic for them."
That gossip protocol is BGP, Border Gateway Protocol. Designed out in 1989 on the backs of a few napkins, it still holds the internet together to this day.
The addresses they gossip about come in blocks called prefixes, each one looks like 194.65.0.0/16. Think of it as a postal code range, that one covers 65,536 addresses and belongs to MEO.
So when you load a website, your packets follow a chain of these announcements. Each hop is one network trusting the other one.
BGP has almost no built-in way to verify that a claim is true. If a network says "I can reach those addresses", its neighbors mostly just… believe it. It works by mutual trust and that is how route leaks and hijacks happen, and it's why BGP incidents keep making the news.
Two more terms, then we're done with theory:
- Transit provider, a big network you pay to carry your traffic everywhere else. Arelion, Cogent, Hurricane Electric are examples.
- Route leak, when a network passes along routes it shouldn't have.
How MEO is actually wired
Before we can talk about what broke, you need to see the shape of the thing.
MEO doesn't run one network, it runs several, each with its own AS number. Straight from RIPE's database:
AS3243 MEO-RESIDENCIAL MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.
AS8657 MEO-INTERNACIONAL MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.
AS42863 MEO-MOVEL MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.
AS15525 MEO-EMPRESAS MEO - SERVICOS DE COMUNICACOES E MULTIMEDIA S.A.Four networks: homes, backbone, mobile, business.
Now the interesting bit. I asked RIPE who sits between each of them and the rest of the internet:
$ curl -s "https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS3243" \
| jq -r '.data.neighbours[] | select(.type=="left") | "\(.asn) strength \(.power)"'
8657 strength 2714That's it. One answer. Same story for mobile and business, every one of them reaches the world through AS8657.
Look at that middle box. AS8657 owns only four prefixes of its own:
Four. Meanwhile residential has 17, mobile has 10, business has 45, and all 72 of those reach the internet through the four-prefix network's connections.
This confuses people, so: owning address space and carrying traffic are completely different jobs. Think of it like a railway company, it might own four stations while running trains to a thousand destinations.
Which means if AS8657 has a bad day, everyone has a bad day. Hold that thought.
How bad was it
Georgia Tech runs a project called IODA that continuously probes address blocks worldwide and records which ones answer.
Now pull raw signals for MEO residential:
$ curl -s "https://api.ioda.inetintel.cc.gatech.edu/v2/signals/raw/asn/3243?from=1789394400&until=1789419600&sourceParams=" \
| jq '.data[0][] | select(.datasource=="ping-slash24") | .values'
[5938, 5933, 5930, 5933, 5933, 5923, 5928, 5930, 5933, 5932, 5932, 5937,
5933, 5930, 5287, 2905, 1670, 1039, 368, 817, 524, 3957, 5467, 5648,
5834, 5917, 5932, 5939, 5936, 5941, 5939, 5946, 5944, 5942, 5947, 5944]Read that left to right. Cruising at ~5,930 responsive blocks… then 5287 → 2905 → 1670 → 1039 → 368.
From 5,933 down to 368. A 93.8% collapse, bottoming out at 18:00 WEST.
Same query for the others:
Network | Role | Normal | Worst | Drop |
|---|---|---|---|---|
AS3243 | Residential | 5,933 | 368 | -93.8% |
AS42863 | Mobile | 606 | 42 | -93.1% |
AS15525 | Business | 481 | 19 | -96.0% |
AS8657 | International | 6 | 0 | -100% |
And packet loss, from the same response:
$ curl -s "https://api.ioda.inetintel.cc.gatech.edu/v2/signals/raw/asn/3243?from=1789394400&until=1789419600&sourceParams=" \
| jq '[.data[0][] | select(.datasource=="ping-slash24-loss")
| .values[][0].agg_values.loss_pct]'
[9.53, 9.59, 9.63, 9.64, 9.58, 9.69, 9.63, 9.6, 9.51, 9.51, 9.51, 9.5,
9.47, 9.45, 19.4, 57.47, 77.26, 88.76, 97.49, 95.41, 93.91, 72.84,
26.87, 16.51, 13.93, 10.93, 9.65, 9.48]Baseline loss ~9.5%. Peak loss 97.49%.
MEO just vanished.
Now the routing side, this is where it gets fun
RIPE runs the Routing Information Service, a fleet of collectors worldwide that record BGP messages as they happen.
First question: was there unusual BGP activity? Let's compare the incident day against the same hours the day before.
$ for day in 2026-09-13 2026-09-14; do
printf "%s " "$day"
curl -s "https://stat.ripe.net/data/bgp-updates/data.json?resource=AS3243&starttime=${day}T14:00:00&endtime=${day}T21:00:00" \
| jq '.data.nr_updates'
done
2026-09-13 545
2026-09-14 72679545 → 72,679. That's 133 times the normal BGP “flow”.
Across all four networks:
Network | 13 Sep | 14 Sep | Multiplier |
|---|---|---|---|
AS3243 Residential | 545 | 72,679 | 133× |
AS8657 International | 350 | 19,428 | 56× |
AS42863 Mobile | 460 | 59,604 | 130× |
AS15525 Business | 1,377 | 223,374 | 162× |
Something shook the routing table hard. Where exactly? There's an endpoint that buckets it by time:
$ curl -s "https://stat.ripe.net/data/bgp-update-activity/data.json?resource=AS3243&starttime=2026-09-14T16:00:00&endtime=2026-09-14T18:40:00&min_sampling_period=600&hide_empty_samples=false" \
| jq -r '.data.updates[] | "\(.starttime[11:16])Z \(.announcements)"'
16:00Z 0
16:10Z 0
16:20Z 250
16:30Z 1069
16:40Z 9724
16:50Z 3824
17:00Z 30458 ← peak
17:10Z 10089
17:20Z 9334
17:30Z 6325
17:40Z 1
17:50Z 9
18:00Z 86830k announcements in ten minutes, right at 17:00 UTC (18:00 WEST), the exact moment packet loss peaked. The data plane and the control plane broke together.
The single best piece of evidence
This is my favourite bit. I took one real MEO prefix (194.65.0.0/16, where www.meo.pt lives) and pulled the global routing state at three moments: before, during, after.
$ for ts in 16:00 17:10 18:30; do
echo "=== $ts UTC ==="
curl -s "https://stat.ripe.net/data/bgp-state/data.json?resource=194.65.0.0%2F16×tamp=2026-09-14T${ts}:00" \
| jq -r '.data.bgp_state[]
| select(.source_id | test("195.66.227.163|80.81.192.194|12.0.1.63|208.51.134.248"))
| "\(.source_id) \(.path|join(" "))"'
doneOutput:
=== 16:00 UTC ===
01-195.66.227.163 1299 8657 3243
12-80.81.192.194 6762 8657 3243
00-12.0.1.63 7018 1299 8657 3243
00-208.51.134.248 3549 3356 1299 8657 3243
=== 17:10 UTC ===
01-195.66.227.163 1299 174 8657 3243
12-80.81.192.194 6762 174 8657 3243
00-12.0.1.63 7018 174 8657 3243
00-208.51.134.248 3549 3356 174 8657 3243
=== 18:30 UTC ===
01-195.66.227.163 1299 8657 3243
12-80.81.192.194 6762 8657 3243
00-12.0.1.63 7018 1299 8657 3243
00-208.51.134.248 3549 3356 1299 8657 3243Do you see it?
174 appears in every single path during the incident, then vanishes again afterwards.
The aggregate confirms it:
Time (UTC) | Routes seen | via AS6939 | via AS174 | via AS1299 |
|---|---|---|---|---|
16:00, before | 384 | 164 | 17 | 30 |
17:10, during | 327 | 19 | 207 | 0 |
18:30, after | 385 | 163 | 24 | 24 |
Hurricane Electric went from 164 views to 19. Arelion went to zero. Cogent went from 17 to 207.
That is what a network looks like when most of its doors slam shut and everyone piles through the one that's left.
Why did Cogent survive while Arelion/HE largely disappeared? I’m also not sure, but if I would guess, physical interconnection paths or different capacity, maybe MEO deliberately withdraw Arelion/HE.. we will never know.
Why everyone talked about Ghana
While I was writing this up, someone pointed me at multiple posts on X claiming the whole thing was a BGP leak involving Ghana Telecom.
Cloudflare Radar runs an automatic route-leak detector, and it had logged event 614156:
Ghana Telecom took routes it learned from Angola Cables and advertised them to MEO. A textbook provider-customer-provider leak.
MEO is one of Ghana Telecom's transit providers, Ghana sat between two providers and passed one's routes to the other.
Most networks give routes learned from a customer the highest preference, because customers pay you. Ghana is MEO's customer. So a leaked route arriving from Ghana can beat a perfectly good route from a peer on preference alone, before path length is even considered. In MEO case i believe this was not the case, because that leak only became “public” when all the other routes went down.
Reading the actual BGP messages
RIPE publishes every BGP message its collectors receive as MRT dumps (RFC 6396), five-minute files, going back years. Nothing is summarised for you here; it's the wire format, exactly as the routers spoke it.
To read it you want bgpdump, RIPE's own C tool.
The files are named by collector and five-minute slot:
$ curl -s "https://data.ris.ripe.net/rrc00/2026.09/" \
| grep -o 'updates\.20260914\.17[0-9][0-9]\.gz' | head -4
updates.20260914.1700.gz
updates.20260914.1705.gz
updates.20260914.1710.gz
updates.20260914.1715.gzI pulled 28 of them, four collectors (rrc00 multihop, rrc01 LINX London, rrc03 AMS-IX, rrc12 DE-CIX Frankfurt) across both moments where Cloudflare said something happened. 122 MB of compressed BGP.
Then the entire search is one grep:
$ bgpdump -m rrc01.1730.gz | grep '29614 37468'-m gives one pipe-delimited line per message, and because the AS path is just text in that line, "Ghana Telecom announcing Angola Cables' routes" is literally the string 29614 37468.
Here's real output, untouched:
BGP4MP|1789348431|A|195.66.227.160|3214|102.208.80.0/24|
3214 8657 29614 37468 174 37282 329486|IGP|195.66.227.160|0|0||NAG||
BGP4MP|1789407023|A|195.90.96.2|48185|169.239.249.0/24|
48185 8657 29614 29614 29614 29614 29614 37468 37282
327992 327992 327992 327992 327992|IGP|195.90.96.2|0|0||NAG||Run it across all 28 files:
$ for g in *.gz; do bgpdump -m "$g"; done | grep '29614 37468' > leak.txt
$ wc -l < leak.txt
146
$ awk -F'|' '{print $6}' leak.txt | sort -u
102.208.80.0/24
169.239.248.0/24
169.239.249.0/24
169.239.250.0/24
169.239.251.0/24
$ awk -F'|' '{n=split($7,a," "); print a[n]}' leak.txt | sort -u
327992
329486
$ awk -F'|' '{print $4}' leak.txt | sort -u | wc -l
30Five prefixes. Two origin ASNs, exactly the same Cloudflare's event said.
And the timing, pulled from field 2:
$ awk -F'|' '{print $6, strftime("%H:%M:%S",$2,1)}' leak.txt \
| sort -u -k1,1 -k2,2Prefix | First seen (UTC) | Last seen (UTC) | Announcements | RIS peers |
|---|---|---|---|---|
| 01:13:51 | 17:31:38 | 22 | 11 |
| 17:30:23 | 17:37:49 | 30 | 29 |
| 17:30:23 | 17:37:49 | 32 | 29 |
| 17:30:23 | 17:37:49 | 32 | 29 |
| 17:30:23 | 17:37:49 | 30 | 29 |
Cloudflare's event window was 01:13 → 17:37. The dumps say 01:13:51 → 17:37:49, perfect.
That bank prefix is the interesting one: announced at 01:13 and still being re-announced at 17:31, sixteen hours later. It sat in the table all day.
So who actually got leaked?
Prefix | Origin AS | Who that really is | Country |
|---|---|---|---|
| AS329486 | GCB Bank PLC, Ghana's largest indigenous bank | GH |
| AS327992 | GARNET, Ghanaian Academic & Research Network | GH |
| AS327992 | GARNET, NOC block | GH |
| AS327992 | GARNET, point-to-point block | GH |
| AS327992 | GARNET, customer point-to-point | GH |
A Ghanaian bank and a Ghanaian university network. Five /24s, 1,280 IP addresses in total.
Not a CDN, not a cloud region, certainty not Portugal.
The full path
Here's one of the real paths, lifted straight out of the dump:
1103 8657 29614 37468 174 37282 329486
│ │ │ │ │ │ └─ GCB Bank, Ghana
│ │ │ │ │ └───────── MainOne Cable, Nigeria
│ │ │ │ └──────────────── Cogent
│ │ │ └─────────────────────── Angola Cables
│ │ └─────────────────────────────── Ghana Telecom ← leak happens here
│ └────────────────────────────────────── MEO International
└───────────────────────────────────────────── SURFnet (the RIS peer that logged it)A Ghanaian bank's traffic went via Nigeria, via Cogent, via Angola, back through Ghana, into Portugal, then out to the world. That's a spectacularly stupid path for packets to take, and it is exactly what a route leak looks like.
The GARNET one is even more revealing, look at the prepending:
3257 8657 29614 29614 29614 29614 29614 37468 37282 327992 327992 327992 327992 327992
└──────── ×5 ───────────────┘ └────────────── ×5 ──────┘Ghana Telecom prepended itself five times, GARNET prepended itself five times. Prepending is how you make a route less attractive.
The leaks were always there
Remember MEO's three attack windows? Convert them to UTC and check where every leak event lands:
wave 1 00:45-02:55 WEST = 23:45-01:55 UTC
wave 2 17:30-19:30 WEST = 16:30-18:30 UTC
wave 3 later thatd day
102.208.80.0/24 first 01:13:51 UTC -> INSIDE wave 1
102.208.80.0/24 last 17:31:38 UTC -> INSIDE wave 2
169.239.248-251 first 17:30:23 UTC -> INSIDE wave 2
169.239.248-251 last 17:37:49 UTC -> INSIDE wave 2Every single leak event lands inside a declared attack window. Not near one, inside.
The bank prefix leaked during wave 1, sat quietly for sixteen hours, then got re-announced during wave 2. The GARNET prefixes appeared 23 seconds after wave 2 started.
That is not what an independent accident looks like. It's what you'd expect when BGP sessions are flapping under load and routers are frantically re-announcing whatever they still hold.
Would RPKI have stopped it?
No, and this is the part worth understanding.
$ curl -s "https://stat.ripe.net/data/rpki-validation/data.json?resource=AS327992&prefix=169.239.249.0/24" \
| jq -r '.data.status'
valid
$ curl -s "https://stat.ripe.net/data/rpki-validation/data.json?resource=AS329486&prefix=102.208.80.0/24" \
| jq -r '.data.status'
unknownThe GARNET prefixes are RPKI-valid. GARNET published a signed statement saying "AS327992 may announce this", and AS327992 is the origin in the leaked path. Everything checks out cryptographically.
Because RPKI only validates the origin, the last hop. It says nothing about the middle of the path. The origin here was completely legitimate, the wrongdoing was in the Ghanian announcements.
That's why RPKI alone doesn't fix leaks, and why people are working on ASPA. It lets you also sign who your providers are, so a leak like this becomes detectable in the path rather than only at the origin.
So was Ghana the cause of the outage?
No. Now we can say it with numbers instead of hand-waving.
The leak moved 1,280 IP addresses belonging to a bank and a university in Ghana. The outage made roughly 93-96% of MEO's entire address space unreachable.
The leak was real and it was a side effect, not the cause.
How a DDoS breaks routing
BGP sessions are just TCP connections. Two routers agree to chat, then send each other small keepalive messages to prove they're still alive. Typically if you miss three in a row, around 90 seconds of silence, your neighbor declares you dead and throws away every route you ever told it about.
That single failure ripples outward. Every network that learned MEO's routes via Arelion now has to pick a different path. They all recompute. They tell their neighbors. And you get 72,679 announcements where you'd normally see 545.
And crucially, when the good paths disappear, whatever is left wins. Including a pre-existing leaked route from Ghana that had been sitting there quietly losing the best-path contest all along.
What MEO and the regulator finally said
On Tuesday, MEO published a much more detailed statement, and it's worth quoting properly. Via RTP/Lusa:
Os incidentes "ocorreram nos períodos, 00:45-02:55, 17:30-19:30 e 22:45-01:05, tendo como alvo específico infraestrutura da Meo, e envolveram elevados volumes de tráfego que provocaram congestionamento e degradação temporária da rede internacional".
And on the BGP question specifically:
"Instabilidade nos encaminhamentos BGP foi consequência do ataque e não a sua causa."
("BGP routing instability was a consequence of the attack, not its cause.")
ANACOM, the Portuguese regulator, backed this up. It told Expresso that a cyberattack is confirmed, and that a DDoS aimed specifically at MEO's international interconnections is the probable cause while analysis continues. They also noted the congestion caused knock-on degradation at other operators' interconnections, which explains why NOS and Vodafone customers saw a few hundred complaints each despite not being attacked.
The incident was reported to Portugal's CNCS (national cybersecurity center), as legally required.
What we still don't know
- How big was the attack? No bandwidth figure, no packet rate. 200 Gbps or 2 Tbps? Unknown.
- What kind of attack? UDP flood? DNS reflection? Carpet bomb across many prefixes? Unknown.
- Who did it? No attribution, no claimed responsibility.
- What exactly was targeted? "International interconnections" is vague. Which routers? Which interfaces?
- Why did Ghana Telecom leak? A filter change, a session reset, an automation bug? The five-times prepending suggests someone was already mid-fire-fight, but that's inference.
The takeaway
Three things worth remembering:
1. Shared dependencies are invisible until they break. MEO has serious diversity at the edge, Hurricane, Arelion, Cogent, Sparkle, RETN, exchange points in London, Frankfurt, Amsterdam, Madrid, Marseille. None of it helped, because residential, mobile and business all converge on AS8657 in the public routing view.
2. Correlation during an incident is a trap. The Ghana leak was real, it was concurrent, both bursts landed inside the attack windows, and it still wasn't the cause. If you'd stopped investigating the moment you found it, you'd have published something confidently wrong.
3. You can check this stuff yourself. RIPE, IODA, Cloudflare Radar, PeeringDB, all free, all open, all queryable from a laptop with curl. The internet's routing system is remarkably transparent if you know where to look.
All measurements in this post were taken on 14-15 September 2026.

No comments yet